#include"searcher.h"
#include"../common/util.hpp"
using namespace std;


const char* const DICT_PATH = "../jieba_dict/jieba.dict.utf8";
const char* const HMM_PATH = "../jieba_dict/hmm_model.utf8"; 
const char* const USR_DICT_PATH = "../jieba_dict/user.dict.utf8"; 
const char* const IDF_PATH = "../jieba_dict/idf.utf8"; 
const char* const STOP_WORD_PATH = "../jieba_dict/stop_words.utf8"; 



namespace searcher
{
  Index::Index()
       :jieba(DICT_PATH,HMM_PATH,USR_DICT_PATH,IDF_PATH,STOP_WORD_PATH){}


  const DocInfo* Index::GetDocInfo(int64_t doc_id)
  {
    if(doc_id <= 0 || doc_id >= forward_index.size())
    {
      return nullptr;
    }
    return &forward_index[doc_id];
  }

  const Index::InvertedList* Index::GetInvertedList(const string& key)
  {
    //用迭代器直接查找即可
    unordered_map<string,InvertedList>::iterator it = inverted_index.find(key);
    //auto it = inverted_index.find(key);
    if(it == inverted_index.end())
    {
      return nullptr;
    }
    return &it->second;
  }

  bool Index::Build(const string & input_path)
  {
    cout<<"开始构建索引"<<endl;
    //预处理模块parser解析到的row_input行文本文件，每一行对应一个html，且由'\3'分割
    //1.读取row_input的每一行
    std::ifstream file(input_path.c_str());
    if(!file.is_open())
    {
      cout<<"row_input文件打开失败~~"<<endl;
      return false;
    }
    string line;
    while(std::getline(file,line))
    {
      //2.针对当前行进行解析，解析成DocInfo对象，构造正排索引
      DocInfo* doc_info = BuildForward(line);
      if(doc_info == nullptr)
      {
        //当前文档正排出现问题
        cout<<"构建正排失败~~"<<endl;
        continue;   //容忍部分错误
      }
      //3.针对当前返回的doc_info对象，进行解析（分词），得到倒排索引
      BuildInverted(*doc_info);

      
      
      
      //打印日志，不能太多，也不能太少
      if(doc_info->doc_id % 100 == 0)
      {
        cout<< doc_info->doc_id <<endl;
      }
    
    }


    cout<<"结束构建索引"<<endl;
    file.close();
    return true;
  }

  
  //核心操作：获取到的line是由'\3'分割的三个部分，标题、url、content
  //C++标准库中提供的strtok，使用起来非常麻烦，1.需要循环 2.会修改原来的字符串 3.缺少一些必要的选项来控制行为 4.线程不安全
  //这里使用boost库中的split来对字符串进行切分（在common::util中对split作简单封装）
  DocInfo* Index::BuildForward(const string& line)
  {
      //1.先把当前行按照'\3'切成三个部分
      vector<string> tokens;
      common::util::Split(line,"\3",tokens);//由于切分符号函数参数是string，所以\3必须带""（字符串），不能''（字符）
      if(tokens.size() != 3)
      {
        //如果切分结果不是三份，就认为当前文档是不对的，构建失败
        return nullptr;
      }
      //2.将结果保存在DocInfo对象中返回
      DocInfo doc_info;
      //doc_id应该是正排索引的末尾位置的下一个位置即forward_index.size();
      doc_info.doc_id = forward_index.size();
      doc_info.title = tokens[0];
      doc_info.url = tokens[1];
      doc_info.content = tokens[2];
      //将doc_info插入正排索引表
      //forward_index.push_back(doc_info);   
                      //forward是一个vector<DocInfo>，doc_info要先拷贝构造才能进行push_back
                      //但doc_info可能是一个较大的副本，如果拷贝构造实在太吃力
                      //发现这里doc_info是一个临时变量，完了就不用了，完全可以使用移动语义，将这块空间移动给forword_index[size]
                      //std::string::push_back()提供了移动语义的版本，直接将doc_info转为右值即可
      forward_index.push_back(std::move(doc_info));

      //3.返回结果（新的doc_info的地址）
      //return &doc_info;      这种做法是典型错误，这是返回了一个临时变量的地址，临时变量会被销毁,只能拿到野指针
      return &forward_index.back(); 
  }


  //倒排是一个hash表
  //key是词（针对文档的分词结果）
  //value是倒排拉链（包含若干个Weight对象）  每个Weight对象中右包含权重（相关性）
  //每次遍历这个文档，就要分析这个文档，并且把相关信息都更新到倒排结构中
  void Index::BuildInverted(const DocInfo& doc_info)
  {
    //1.分词时，要同时考虑标题和文档(设置一个结构体分别记录标题和正文中某词出现的次数)
    //2.在构造Weight时要考虑权重字段如何计算（相关性）
              //这里使用简单粗暴的方法： weight = 标题出现次数 * 10 + 正文出现次数
              //商业搜索引擎中，出现次数确实也是考虑因素之一
              //突出标题的重要性，体现：A文档标题中只出现1次，而B文档正文中出现了10次，依然认为A的权重（相关性）高
  
    //创建专门用于统计某词汇在标题和正文中出现的次数
    struct WordCnt
    {
      int title_cnt;
      int content_cnt;
      WordCnt():title_cnt(0),content_cnt(0)
      {}
    };
    //使用hash表（key为string，value为WordCnt）来表示统计的结果
    unordered_map<string,WordCnt> word_cnt_map;
    
    vector<string> title_token;
    //1.对doc_info.title进行分词，
    CutWord(doc_info.title,title_token); 
    //2.遍历分词结果,统计每个词出现的次数
    for(string word: title_token)
    {
      //把title_token[]中的每个词放在map中，并统计在标题中出现的次数
      //map/unordered_map的方括号[]有两个功能，存在则修改，不存在则添加
      //在添加之前，需要考虑大小写的问题，
      //对于HELLO、hello和Hello对应的三种结果相同吗？    相同的     
      //使用boost::to_lower全部转换为小写字母（std中没有）
      boost::to_lower(word);            //此处要对word进行修改，则rangebasefor中的auto word不能加const，为了避免不必要的麻烦，这里直接进行显示定义，string word 
      word_cnt_map[word].title_cnt++;
    }
    //3.对doc_info.content进行分词,并统计次数
    vector<string> content_token;
    CutWord(doc_info.content,content_token);
    //4.遍历分词结果，统计每个词出现的次数
    for(string word : content_token)
    {
      boost::to_lower(word);
      word_cnt_map[word].content_cnt++;
    }
    //5.根据统计结果，整合Weight对象信息，更新到倒排索引中
    //   word_pair 每次循环就是对应到map的一个键值对（std::pair）
    for(const auto& word_pair : word_cnt_map)
    {
      //构造Weight对象
      Weight weight;
      weight.doc_id = doc_info.doc_id;
      //权重 = 10 * 词在标题中出现次数 + 词在正文中出现次数
      weight.weight = 10 * word_pair.second.title_cnt + word_pair.second.content_cnt;
      weight.word = word_pair.first;

      //把Weight对象插入倒排索引中，
      //需要先找到对应的倒排拉链，然后追加到拉链末尾
      //  inverted_index[weight.word].push_back(weight);
      InvertedList& inverted_list = inverted_index[word_pair.first];
      inverted_list.push_back(weight);      //weight 是小对象，拷贝构造开销不大，可以不适用std::move()  
    }
  } 



  void Index::CutWord(const string& input,vector<string>& output)
  {
    jieba.CutForSearch(input,output);
  }




}//namespace sercher
